<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="dom-api.html">
<link rel="import" href="style-transformer.html">
<link rel="import" href="style-util.html">
<link rel="import" href="settings.html">

<script>

  Polymer.StyleProperties = (function() {
    'use strict';

    var matchesSelector = Polymer.DomApi.matchesSelector;
    var styleUtil = Polymer.StyleUtil;
    var styleTransformer = Polymer.StyleTransformer;
    var IS_IE = navigator.userAgent.match('Trident');

    var settings = Polymer.Settings;

    return {

      // decorates styles with rule info and returns an array of used style
      // property names
      decorateStyles: function(styles, scope) {
        var self = this, props = {}, keyframes = [], ruleIndex = 0;
        var scopeSelector = styleTransformer._calcHostScope(scope.is, scope.extends);
        styleUtil.forRulesInStyles(styles, function(rule, style) {
          self.decorateRule(rule);
          // mark in-order position of ast rule in styles block, used for cache key
          rule.index = ruleIndex++;
          self.whenHostOrRootRule(scope, rule, style, function(info) {
            // we can't cache styles with :host and :root props in @media rules
            if (rule.parent.type === styleUtil.ruleTypes.MEDIA_RULE) {
              scope.__notStyleScopeCacheable = true;
            }
            if (info.isHost) {
              // check if the selector is in the form of `:host-context()` or `:host()`
              // if so, this style is not cacheable
              var hostContextOrFunction = info.selector.split(' ').some(function(s) {
                return s.indexOf(scopeSelector) === 0 && s.length !== scopeSelector.length;
              });
              scope.__notStyleScopeCacheable = scope.__notStyleScopeCacheable || hostContextOrFunction;
            }
          });
          self.collectPropertiesInCssText(rule.propertyInfo.cssText, props);
        }, function onKeyframesRule(rule) {
          keyframes.push(rule);
        });
        // Cache all found keyframes rules for later reference:
        styles._keyframes = keyframes;
        // return this list of property names *consumes* in these styles.
        var names = [];
        for (var i in props) {
          names.push(i);
        }
        return names;
      },

      // decorate a single rule with property info
      decorateRule: function(rule) {
        if (rule.propertyInfo) {
          return rule.propertyInfo;
        }
        var info = {}, properties = {};
        var hasProperties = this.collectProperties(rule, properties);
        if (hasProperties) {
          info.properties = properties;
          // TODO(sorvell): workaround parser seeing mixins as additional rules
          rule.rules = null;
        }
        info.cssText = this.collectCssText(rule);
        rule.propertyInfo = info;
        return info;
      },

      // collects the custom properties from a rule's cssText
      collectProperties: function(rule, properties) {
        var info = rule.propertyInfo;
        if (info) {
          if (info.properties) {
            Polymer.Base.mixin(properties, info.properties);
            return true;
          }
        } else {
          var m, rx = this.rx.VAR_ASSIGN;
          var cssText = rule.parsedCssText;
          var value;
          var any;
          while ((m = rx.exec(cssText))) {
            // note: group 2 is var, 3 is mixin
            value = (m[2] || m[3]).trim();
            // value of 'inherit' is equivalent to not setting the property here
            if (value !== 'inherit') {
              properties[m[1].trim()] = value;
            }
            any = true;
          }
          return any;
        }
      },

      // returns cssText of properties that consume variables/mixins
      collectCssText: function(rule) {
        return this.collectConsumingCssText(rule.parsedCssText);
      },

      // NOTE: we support consumption inside mixin assignment
      // but not production, so strip out {...}
      collectConsumingCssText: function(cssText) {
        return cssText.replace(this.rx.BRACKETED, '')
          .replace(this.rx.VAR_ASSIGN, '');
      },

      collectPropertiesInCssText: function(cssText, props) {
        var m;
        while ((m = this.rx.VAR_CONSUMED.exec(cssText))) {
          var name = m[1];
          // This regex catches all variable names, and following non-whitespace char
          // If next char is not ':', then variable is a consumer
          if (m[2] !== ':') {
            props[name] = true;
          }
        }
      },

      // turns custom properties into realized values.
      reify: function(props) {
        // big perf optimization here: reify only *own* properties
        // since this object has __proto__ of the element's scope properties
        var names = Object.getOwnPropertyNames(props);
        for (var i=0, n; i < names.length; i++) {
          n = names[i];
          props[n] = this.valueForProperty(props[n], props);
        }
      },

      // given a property value, returns the reified value
      // a property value may be:
      // (1) a literal value like: red or 5px;
      // (2) a variable value like: var(--a), var(--a, red), or var(--a, --b) or
      // var(--a, var(--b));
      // (3) a literal mixin value like { properties }. Each of these properties
      // can have values that are: (a) literal, (b) variables, (c) @apply mixins.
      valueForProperty: function(property, props) {
        // case (1) default
        // case (3) defines a mixin and we have to reify the internals
        if (property) {
          if (property.indexOf(';') >=0) {
            property = this.valueForProperties(property, props);
          } else {
            // case (2) variable
            var self = this;
            var fn = function(prefix, value, fallback, suffix) {
              var propertyValue = self.valueForProperty(props[value], props);
              // if value is "initial", then the variable should be treated as unset
              if (!propertyValue || propertyValue === 'initial') {
                // fallback may be --a or var(--a) or literal
                propertyValue = self.valueForProperty(props[fallback] || fallback, props) ||
                fallback;
              } else if (propertyValue === 'apply-shim-inherit') {
                // CSS build will replace `inherit` with `apply-shim-inherit`
                // for use with native css variables.
                // Since we have full control, we can use `inherit` directly.
                propertyValue = 'inherit';
              }
              return prefix + (propertyValue || '') + suffix;
            };
            property = styleUtil.processVariableAndFallback(property, fn);
          }
        }
        return property && property.trim() || '';
      },

      // note: we do not yet support mixin within mixin
      valueForProperties: function(property, props) {
        var parts = property.split(';');
        for (var i=0, p, m; i<parts.length; i++) {
          if ((p = parts[i])) {
            this.rx.MIXIN_MATCH.lastIndex = 0;
            m = this.rx.MIXIN_MATCH.exec(p);
            if (m) {
              p = this.valueForProperty(props[m[1]], props);
            } else {
              var colon = p.indexOf(':');
              if (colon !== -1) {
                var pp = p.substring(colon);
                pp = pp.trim();
                pp = this.valueForProperty(pp, props) || pp;
                p = p.substring(0, colon) + pp;
              }
            }
            parts[i] = (p && p.lastIndexOf(';') === p.length - 1) ?
              // strip trailing ;
              p.slice(0, -1) :
              p || '';
          }
        }
        return parts.join(';');
      },

      applyProperties: function(rule, props) {
        var output = '';
        // dynamically added sheets may not be decorated so ensure they are.
        if (!rule.propertyInfo) {
          this.decorateRule(rule);
        }
        if (rule.propertyInfo.cssText) {
          output = this.valueForProperties(rule.propertyInfo.cssText, props);
        }
        rule.cssText = output;
      },

      // Apply keyframe transformations to the cssText of a given rule. The
      // keyframeTransforms object is a map of keyframe names to transformer
      // functions which take in cssText and spit out transformed cssText.
      applyKeyframeTransforms: function(rule, keyframeTransforms) {
        var input = rule.cssText;
        var output = rule.cssText;
        if (rule.hasAnimations == null) {
          // Cache whether or not the rule has any animations to begin with:
          rule.hasAnimations = this.rx.ANIMATION_MATCH.test(input);
        }
        // If there are no animations referenced, we can skip transforms:
        if (rule.hasAnimations) {
          var transform;
          // If we haven't transformed this rule before, we iterate over all
          // transforms:
          if (rule.keyframeNamesToTransform == null) {
            rule.keyframeNamesToTransform = [];
            for (var keyframe in keyframeTransforms) {
              transform = keyframeTransforms[keyframe];
              output = transform(input);
              // If the transform actually changed the CSS text, we cache the
              // transform name for future use:
              if (input !== output) {
                input = output;
                rule.keyframeNamesToTransform.push(keyframe);
              }
            }
          } else {
            // If we already have a list of keyframe names that apply to this
            // rule, we apply only those keyframe name transforms:
            for (var i = 0; i < rule.keyframeNamesToTransform.length; ++i) {
              transform = keyframeTransforms[rule.keyframeNamesToTransform[i]];
              input = transform(input);
            }
            output = input;
          }
        }
        rule.cssText = output;
      },

      // Test if the rules in these styles matches the given `element` and if so,
      // collect any custom properties into `props`.
      propertyDataFromStyles: function(styles, element) {
        var props = {}, self = this;
        // generates a unique key for these matches
        var o = [];
        // note: active rules excludes non-matching @media rules
        styleUtil.forActiveRulesInStyles(styles, function(rule) {
          // TODO(sorvell): we could trim the set of rules at declaration
          // time to only include ones that have properties
          if (!rule.propertyInfo) {
            self.decorateRule(rule);
          }
          // match element against transformedSelector: selector may contain
          // unwanted uniquification and parsedSelector does not directly match
          // for :host selectors.
          var selectorToMatch = rule.transformedSelector || rule.parsedSelector;
          if (element && rule.propertyInfo.properties && selectorToMatch) {
            if (matchesSelector.call(element, selectorToMatch)) {
              self.collectProperties(rule, props);
              // produce numeric key for these matches for lookup
              addToBitMask(rule.index, o);
            }
          }
        });
        return {properties: props, key: o};
      },

      _rootSelector: /:root|:host\s*>\s*\*/,

      _checkRoot: function(hostScope, selector) {
        return Boolean(selector.match(this._rootSelector)) ||
          (hostScope === 'html' && selector.indexOf('html') > -1);
      },

      whenHostOrRootRule: function(scope, rule, style, callback) {
        if (!rule.propertyInfo) {
          self.decorateRule(rule);
        }
        if (!rule.propertyInfo.properties) {
          return;
        }
        var hostScope = scope.is ?
        styleTransformer._calcHostScope(scope.is, scope.extends) :
        'html';
        var parsedSelector = rule.parsedSelector;
        var isRoot = this._checkRoot(hostScope, parsedSelector);
        var isHost = !isRoot && parsedSelector.indexOf(':host') === 0;
        // build info is either in scope (when scope is an element) or in the style
        // when scope is the default scope; note: this allows default scope to have
        // mixed mode built and unbuilt styles.
        var cssBuild = scope.__cssBuild || style.__cssBuild;
        if (cssBuild === 'shady') {
          // :root -> x-foo > *.x-foo for elements and html for custom-style
          isRoot = parsedSelector === (hostScope + ' > *.' + hostScope) || parsedSelector.indexOf('html') > -1;
          // :host -> x-foo for elements, but sub-rules have .x-foo in them
          isHost = !isRoot && parsedSelector.indexOf(hostScope) === 0;
        }
        if (!isRoot && !isHost) {
          return;
        }
        var selectorToMatch = hostScope;
        if (isHost) {
          // need to transform :host under ShadowDOM because `:host` does not work with `matches`
          if (settings.useNativeShadow && !rule.transformedSelector) {
            // transform :host into a matchable selector
            rule.transformedSelector =
            styleTransformer._transformRuleCss(
              rule,
              styleTransformer._transformComplexSelector,
              scope.is,
              hostScope
            );
          }
          // parsedSelector fallback for 'shady' css build
          selectorToMatch = rule.transformedSelector || rule.parsedSelector;
        }
        if (isRoot && hostScope === 'html') {
          selectorToMatch = rule.transformedSelector || rule.parsedSelector;
        }
        callback({
          selector: selectorToMatch,
          isHost: isHost,
          isRoot: isRoot
        });
      },

      hostAndRootPropertiesForScope: function(scope) {
        var hostProps = {}, rootProps = {}, self = this;
        // note: active rules excludes non-matching @media rules
        styleUtil.forActiveRulesInStyles(scope._styles, function(rule, style) {
          // if scope is StyleDefaults, use _element for matchesSelector
          self.whenHostOrRootRule(scope, rule, style, function(info) {
            var element = scope._element || scope;
            if (matchesSelector.call(element, info.selector)) {
              if (info.isHost) {
                self.collectProperties(rule, hostProps);
              } else {
                self.collectProperties(rule, rootProps);
              }
            }
          });
        });
        return {rootProps: rootProps, hostProps: hostProps};
      },

      transformStyles: function(element, properties, scopeSelector) {
        var self = this;
        var hostSelector = styleTransformer
          ._calcHostScope(element.is, element.extends);
        var rxHostSelector = element.extends ?
          '\\' + hostSelector.slice(0, -1) + '\\]' :
          hostSelector;
        var hostRx = new RegExp(this.rx.HOST_PREFIX + rxHostSelector +
          this.rx.HOST_SUFFIX);
        var keyframeTransforms =
          this._elementKeyframeTransforms(element, scopeSelector);
        return styleTransformer.elementStyles(element, function(rule) {
          self.applyProperties(rule, properties);
          if (!settings.useNativeShadow &&
              !Polymer.StyleUtil.isKeyframesSelector(rule) &&
              rule.cssText) {
            // NOTE: keyframe transforms only scope munge animation names, so it
            // is not necessary to apply them in ShadowDOM.
            self.applyKeyframeTransforms(rule, keyframeTransforms);
            self._scopeSelector(rule, hostRx, hostSelector,
              element._scopeCssViaAttr, scopeSelector);
          }
        });
      },

      _elementKeyframeTransforms: function(element, scopeSelector) {
        var keyframesRules = element._styles._keyframes;
        var keyframeTransforms = {};
        if (!settings.useNativeShadow && keyframesRules) {
          // For non-ShadowDOM, we transform all known keyframes rules in
          // advance for the current scope. This allows us to catch keyframes
          // rules that appear anywhere in the stylesheet:
          for (var i = 0, keyframesRule = keyframesRules[i];
               i < keyframesRules.length;
               keyframesRule = keyframesRules[++i]) {
            this._scopeKeyframes(keyframesRule, scopeSelector);
            keyframeTransforms[keyframesRule.keyframesName] =
                this._keyframesRuleTransformer(keyframesRule);
          }
        }
        return keyframeTransforms;
      },

      // Generate a factory for transforming a chunk of CSS text to handle a
      // particular scoped keyframes rule.
      _keyframesRuleTransformer: function(keyframesRule) {
        return function(cssText) {
          return cssText.replace(
              keyframesRule.keyframesNameRx,
              keyframesRule.transformedKeyframesName);
        };
      },

      // Transforms `@keyframes` names to be unique for the current host.
      // Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0
      _scopeKeyframes: function(rule, scopeId) {
        rule.keyframesNameRx = new RegExp(rule.keyframesName, 'g');
        rule.transformedKeyframesName = rule.keyframesName + '-' + scopeId;
        rule.transformedSelector = rule.transformedSelector || rule.selector;
        rule.selector = rule.transformedSelector.replace(
            rule.keyframesName, rule.transformedKeyframesName);
      },

      // Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes):
      // non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo
      // host selector: x-foo.wide -> .x-foo-42.wide
      // note: we use only the scope class (.x-foo-42) and not the hostSelector
      // (x-foo) to scope :host rules; this helps make property host rules
      // have low specificity. They are overrideable by class selectors but,
      // unfortunately, not by type selectors (e.g. overriding via
      // `.special` is ok, but not by `x-foo`).
      _scopeSelector: function(rule, hostRx, hostSelector, viaAttr, scopeId) {
        rule.transformedSelector = rule.transformedSelector || rule.selector;
        var selector = rule.transformedSelector;
        var scope = viaAttr ? '[' + styleTransformer.SCOPE_NAME + '~=' +
          scopeId + ']' :
          '.' + scopeId;
        var parts = selector.split(',');
        for (var i=0, l=parts.length, p; (i<l) && (p=parts[i]); i++) {
          parts[i] = p.match(hostRx) ?
            p.replace(hostSelector, scope) :
            scope + ' ' + p;
        }
        rule.selector = parts.join(',');
      },

      applyElementScopeSelector: function(element, selector, old, viaAttr) {
        var c = viaAttr ? element.getAttribute(styleTransformer.SCOPE_NAME) :
          (element.getAttribute('class') || '');
        var v = old ? c.replace(old, selector) :
          (c ? c + ' ' : '') + this.XSCOPE_NAME + ' ' + selector;
        if (c !== v) {
          if (viaAttr) {
            element.setAttribute(styleTransformer.SCOPE_NAME, v);
          } else {
            element.setAttribute('class', v);
          }
        }
      },

      applyElementStyle: function(element, properties, selector, style) {
        // calculate cssText to apply
        var cssText = style ? style.textContent || '' :
          this.transformStyles(element, properties, selector);
        // if shady and we have a cached style that is not style, decrement
        var s = element._customStyle;
        if (s && !settings.useNativeShadow && (s !== style)) {
          s._useCount--;
          if (s._useCount <= 0 && s.parentNode) {
            s.parentNode.removeChild(s);
          }
        }
        // apply styling always under native or if we generated style
        // or the cached style is not in document(!)
        if (settings.useNativeShadow) {
          // update existing style only under native
          if (element._customStyle) {
            element._customStyle.textContent = cssText;
            style = element._customStyle;
          // otherwise, if we have css to apply, do so
          } else if (cssText) {
            // apply css after the scope style of the element to help with
            // style precedence rules.
            style = styleUtil.applyCss(cssText, selector, element.root,
              element._scopeStyle);
          }
        } else {
          // shady and no cache hit
          if (!style) {
            // apply css after the scope style of the element to help with
            // style precedence rules.
            if (cssText) {
              style = styleUtil.applyCss(cssText, selector, null,
                element._scopeStyle);
            }
          // shady and cache hit but not in document
          } else if (!style.parentNode) {
            if (IS_IE && cssText.indexOf('@media') > -1) {
              // @media rules may be stale in IE 10 and 11
              // refresh the text content of the style to revalidate them.
              style.textContent = cssText;
            }
            styleUtil.applyStyle(style, null, element._scopeStyle);
          }
        }
        // ensure this style is our custom style and increment its use count.
        if (style) {
          style._useCount = style._useCount || 0;
          // increment use count if we changed styles
          if (element._customStyle != style) {
            style._useCount++;
          }
          element._customStyle = style;
        }
        return style;
      },

      // customStyle properties are applied if they are truthy or 0. Otherwise,
      // they are skipped; this allows properties previously set in customStyle
      // to be easily reset to inherited values.
      mixinCustomStyle: function(props, customStyle) {
        var v;
        for (var i in customStyle) {
          v = customStyle[i];
          if (v || v === 0) {
            props[i] = v;
          }
        }
      },

      /* Applies a set of properties to an element's style natively (used to
       * support element.customStyle / element.updateStyles)
       */
      updateNativeStyleProperties: function(element, properties) {
        var oldPropertyNames = element.__customStyleProperties;
        if (oldPropertyNames) {
          // remove previous properties
          for (var i=0; i < oldPropertyNames.length; i++) {
            element.style.removeProperty(oldPropertyNames[i]);
          }
        }
        var propertyNames = [];
        // apply properties
        for (var p in properties) {
          // NOTE: for bc with shim, don't apply null values.
          if (properties[p] !== null) {
            element.style.setProperty(p, properties[p]);
            propertyNames.push(p);
          }
        }
        element.__customStyleProperties = propertyNames;
      },

      rx: styleUtil.rx,
      XSCOPE_NAME: 'x-scope'

    };

    function addToBitMask(n, bits) {
      var o = parseInt(n / 32);
      var v = 1 << (n % 32);
      bits[o] = (bits[o] || 0) | v;
    }

  })();

</script>
